/*
 * Copyright (c) 2020 Fraunhofer FOKUS and others. All rights reserved.
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contact: mosaic@fokus.fraunhofer.de
 */
package org.eclipse.mosaic.fed.carla.clojure;

import com.google.common.collect.Lists;
import libpython_clj2.java_api;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.mosaic.fed.carla.ambassador.ClojureCarlaAmbassador;
import org.eclipse.mosaic.fed.carla.clojure.facades.ClojureSimulationFacade;
import org.eclipse.mosaic.fed.carla.clojure.facades.TrafficLightFacade;
import org.eclipse.mosaic.fed.carla.config.CarlaConfiguration;
import org.eclipse.mosaic.lib.util.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * The CarlaConnection used to connect CARLA simulator and CARLA ambassador
 */
public class ClojureCarlaConnection {

    int carlaRpcPort = 2000;
    String carlaHostName = "localhost";
    ClojureCarlaAmbassador carlaAmbassador;
    String carlaEggPath;
    double updateIntervalSeconds;
    int retryConnectionTimes = 5;
    String map;

    private final ClojureSimulationFacade simulationControl;
    private final TrafficLightFacade trafficLightControl;


    public Object carlaModule;
    public Object carlaClient;
    public Object carlaWorld;
    public Object carlaBlueprintLibrary;
    public List<Double> offset;
    public String vTypesPath;
    public boolean syncVehicleLights;

    public Map<String, Long> sumo2carlaIds = new ConcurrentHashMap<>(10);
    public Map<Long, String> carla2SumoIds = new ConcurrentHashMap<>(10);
    public Map<String, Object> tls = new ConcurrentHashMap<>(10);
    public CommandRegister commandRegister = new CommandRegister();
    public Object spawnedCarlaActors;
    public Object destroyedActors;
    public Object activeActors;

    private static final String CARLA = "carla";
    private static final Logger log = LoggerFactory.getLogger(ClojureCarlaConnection.class);

    /**
     * This is the customized constructor of CarlaConnection
     *
     * @param carlaHostName This is the CarlaConnection server host name
     * @param carlaRpcPort  This the CarlaConnection client port
     * @param fedAmbassador Carla federate ambassador
     */
    public ClojureCarlaConnection(String carlaHostName, int carlaRpcPort, ClojureCarlaAmbassador fedAmbassador,
                                  CarlaConfiguration configuration, int retryConnectionTimes) {
        this(carlaHostName, carlaRpcPort, fedAmbassador, configuration);
        this.retryConnectionTimes = retryConnectionTimes;
    }

    public ClojureCarlaConnection(String carlaHostName, int carlaRpcPort, ClojureCarlaAmbassador fedAmbassador,
                                  CarlaConfiguration configuration) {
        this.carlaHostName = carlaHostName;
        this.carlaRpcPort = carlaRpcPort;
        this.carlaAmbassador = fedAmbassador;
        this.carlaEggPath = configuration.carlaEggPath;
        if (StringUtils.isBlank(configuration.offset)
                || !configuration.offset.contains(",") || configuration.offset.split(",").length != 2) {
            this.offset = Lists.newArrayList(0.0, 0.0);
        } else {
            this.offset = Arrays.stream(configuration.offset.split(","))
                    .map(Double::parseDouble)
                    .collect(Collectors.toList());
        }
        this.updateIntervalSeconds = configuration.updateInterval / 1000.0;
        if (!StringUtils.isBlank(configuration.vTypesPath)) {
            this.vTypesPath = configuration.vTypesPath + "vtypes.json";
        }
        this.syncVehicleLights = configuration.syncVehicleLights;
        this.map = configuration.map;

        this.simulationControl = new ClojureSimulationFacade(this);
        this.trafficLightControl = new TrafficLightFacade(this);
    }

    public void connect() {
        // initialize python library from source root
        java_api.initialize(null);

        try (AutoCloseable locker = java_api.GILLocker()) {
            // load carla egg
            String file = "import glob\n" +
                    "import os\n" +
                    "import sys\n" +
                    "import time\n" +
                    "\n" +
                    "try:\n" +
                    "    sys.path.append(\n" +
                    "        glob.glob('" + carlaEggPath + "carla-*%d.%d-%s.egg' %\n" +
                    "                  (sys.version_info.major, sys.version_info.minor,\n" +
                    "                   'win-amd64' if os.name == 'nt' else 'linux-x86_64'))[0])\n" +
                    "except IndexError:\n" +
                    "    print(\"Can not find carla library .egg\")";
            java_api.runStringAsFile(file);

            carlaModule = java_api.importModule(CARLA);
            Object clientAttr = java_api.getAttr(carlaModule, "Client");
            while (retryConnectionTimes > 0) {
                retryConnectionTimes--;
                boolean connected = true;

                try {
                    carlaClient = java_api.call(clientAttr, carlaHostName, carlaRpcPort);
                    Object setTimeoutAttr = java_api.getAttr(carlaClient, "set_timeout");
                    // set carla client timeout 15s
                    java_api.call(setTimeoutAttr, 15);
                    Object getWorldAttr = java_api.getAttr(carlaClient, "get_world");
                    carlaWorld = java_api.call(getWorldAttr);
                } catch (Exception e) {
                    log.warn("reconnected carla");
                    connected = false;
                }

                if (connected) {
                    log.info("CARLA simulator connected");
                    break;
                }
                if (retryConnectionTimes == 0) {
                    log.error("Maximum connection attempts reached and connecting to CARLA simulator failed.");
                    throw new RuntimeException("Can not connect CARLA server. Check whether CARLA server starts or not.");
                }
            }

            if (!StringUtils.isBlank(map)) {
                Object loadWorld = java_api.getAttr(carlaClient, "load_world");
                java_api.call(loadWorld, map);
                log.info("load map: {}", map);
            }

            Object getBlueprintLibraryAttr = java_api.getAttr(carlaWorld, "get_blueprint_library");
            carlaBlueprintLibrary = java_api.call(getBlueprintLibraryAttr);

            // Configuring carla simulation in sync mode
            Object getSettingsAttr = java_api.getAttr(carlaWorld, "get_settings");
            Object settings = java_api.call(getSettingsAttr);
            java_api.setAttr(settings, "synchronous_mode", Boolean.TRUE);
            java_api.setAttr(settings, "fixed_delta_seconds", updateIntervalSeconds);
            Object applySettingsAttr = java_api.getAttr(carlaWorld, "apply_settings");
            java_api.call(applySettingsAttr, settings);

            // Set global variable
            java_api.setGlobal("blueprint_library", carlaBlueprintLibrary);
            java_api.setGlobal("offset", offset);
            Map vTypes = new HashMap();
            if (!StringUtils.isBlank(vTypesPath)) {
                File vTypesFile = new File(vTypesPath);
                String vTypesString = new String(Files.readAllBytes(Paths.get(vTypesFile.getPath())));
                vTypes =(Map) JsonUtils.deserialize(vTypesString, Map.class).get("carla_blueprints");
                java_api.setGlobal("_VTYPES", vTypes);
            }
            java_api.setGlobal("_VTYPES", vTypes);

            // initialize carla actor set
            spawnedCarlaActors = java_api.runStringAsInput("list()");
            destroyedActors = java_api.runStringAsInput("list()");
            activeActors = java_api.runStringAsInput("list()");

            // initialize traffic light
            Map simulation = java_api.runStringAsFile(
                    "\n" +
                            "import carla\n" +
                            "import logging\n" +
                            "def get_tl(carla_world, landmark):\n" +
                            "    if landmark.id != '':\n" +
                            "        traffic_light = carla_world.get_traffic_light(landmark)\n" +
                            "        if traffic_light is not None:\n" +
                            "            return traffic_light\n" +
                            "        else:\n" +
                            "            logging.warning('Landmark %s is not linked to any traffic light', landmark.id)\n" +
                            "    return None\n" +
                            "def get_landmarks(carla_world):\n" +
                            "    tmp_map = carla_world.get_map()\n" +
                            "    return tmp_map.get_all_landmarks_of_type('1000001')\n" +
                            "def get_landmark_id(landmark):\n" +
                            "    return landmark.id\n" +
                            "def switch_off_traffic_lights(carla_world):\n" +
                            "    for actor in carla_world.get_actors():\n" +
                            "        if actor.type_id == 'traffic.traffic_light':\n" +
                            "            actor.freeze(True)\n" +
                            "            # We set the traffic light to 'green' because 'off' state sets the traffic light to\n" +
                            "            # 'red'.\n" +
                            "            actor.set_state(carla.TrafficLightState.Green)"
            );
            Object getLandmarks = simulation.get("get_landmarks");
            List<Object> landmarks = (List<Object>) java_api.call(getLandmarks, carlaWorld);
            Object getLandmarkId = simulation.get("get_landmark_id");
            Object getTl = simulation.get("get_tl");
            for (Object landmark : landmarks) {
                String landmarkId = (String) java_api.copyToJVM(java_api.call(getLandmarkId, landmark));
                Object trafficLight = java_api.call(getTl, carlaWorld, landmark);
                if (trafficLight != null) {
                    this.tls.put(landmarkId, trafficLight);
                }
            }
            Object switchOffCarlaTrafficLights = simulation.get("switch_off_traffic_lights");
            java_api.call(switchOffCarlaTrafficLights, carlaWorld);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    public void close() {
        try (AutoCloseable locker = java_api.GILLocker()) {
            Object getSettingsAttr = java_api.getAttr(carlaWorld, "get_settings");
            Object settings = java_api.call(getSettingsAttr);
            java_api.setAttr(settings, "synchronous_mode", Boolean.FALSE);
            java_api.setAttr(settings, "fixed_delta_seconds", null);
            Object applySettingsAttr = java_api.getAttr(carlaWorld, "apply_settings");
            java_api.call(applySettingsAttr, settings);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    public ClojureSimulationFacade getSimulationControl() {
        return simulationControl;
    }

    public TrafficLightFacade getTrafficLightControl() {
        return trafficLightControl;
    }
}
